/**
 * @author Ming & Tony
 * Calculator.java
 * Represent an expression or equation.
 */
package calculator;

import java.io.IOException;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;

public class Calculator {

    final static String deleteCommand = "_DEL";

    public Calculator() {
        //this.eval = eval;

        //String str = "123*4logtan1-55.54ln56/678";
        //String str = "-2log100ln5";
        //String str = "2log5";
        //String str = "tansinh1-55.54";
        String str = "55E-2*5";
        System.out.println(str);

        /*replace all the greek letter symbols
        //str = str.replaceAll("\u24B8", "_CONST");
        //System.out.println(str);*/

        //Reader r = new BufferedReader(new InputStreamReader(System.in));


        System.out.println("answer = " + evaluate(str));
    }


    /**
     * evaluates the mathematical expression
     * the priority of each operator in the evaluation process is considered
     * @param expression
     * mathematical expression String to evaluate
     * @return the evaluated answer
     */
    double evaluate(String expression) {

        //Token[] tokens = new Token[evalTokens.size()];
        //evalTokens.toArray(tokens);

        Token[] tokens = tokenizeExpression(expression);

        int highestPriorityIndex = 0;
        int highestPriority = Integer.MAX_VALUE; //the higher the priority, the smaller this number is
        Operator currOperator;
        boolean operatorFound = false;

        while (tokens.length > 1) {
            operatorFound = false;
            highestPriorityIndex = 0;
            highestPriority = Integer.MAX_VALUE;
            for (int index = 0; index < tokens.length; index++) {
                if (tokens[index].getType() != Token.TokenType.OPERATOR_TOKEN) {
                    continue;
                }
                currOperator = new Operator(tokens[index]);
                if (currOperator.getPriority() < highestPriority) {
                    operatorFound = true;
                    highestPriority = currOperator.getPriority();
                    highestPriorityIndex = index;
                }
            }
            //Math eval the tokens[highestPriorityIndex]
            printOutArray(tokens);
            if (!operatorFound) {
                //if no more operators, then add up all the values in the tokens array
                //because 1.0-2.0 is tokenzied as two double values (1.0 and -2.0); the - is not tokenized as a minus operator
                this.noMoreOperators(tokens);
                break;
            }
            tokens = mathEval(tokens, highestPriorityIndex);
            tokens = refreshToEvalArray(tokens);
        }

        System.out.println(tokens[0]);

        return tokens[0].getDoubleValue();
    }

    /**
     *
     * @param toEval
     * the Token[] to evaluate
     * @param operatorIndex
     * the index of the operator in the Token[] to evaluate
     * @return an evaluated Token[] but with whatever that was just evaluated represented as Calculator.deleteCommand
     */
    Token[] mathEval(Token[] toEval, int operatorIndex) {
        String operator = toEval[operatorIndex].toString();
        double answer = 0.0;
        //System.out.println("operatorIndex = " + operatorIndex);

        try {
            if (toEval[operatorIndex + 1].getType() != Token.TokenType.DOUBLE_TOKEN) {
                toEval = mathEval(toEval, operatorIndex + 1);
            //toEval[operatorIndex + 1] = new Token(mathEval(toEval, operatorIndex + 1));
            }
        } catch (ArrayIndexOutOfBoundsException ex) {
            //printOutArray(toEval);
            System.exit(-1);
        }
        toEval = refreshToEvalArray(toEval);
        double numRight = toEval[operatorIndex + 1].getDoubleValue();


        boolean numLeftExists = false;
        try {
            numLeftExists = (toEval[operatorIndex - 1].getType() == Token.TokenType.DOUBLE_TOKEN);
        } catch (ArrayIndexOutOfBoundsException ex) {
        }

        double numLeft = 0.0;
        if (numLeftExists) {
            numLeft = toEval[operatorIndex - 1].getDoubleValue();
        }

        if (operator.equals("E")) {
            answer = numLeft * Math.pow(10, numRight);
        } else if (operator.equalsIgnoreCase("log")) {
            answer = Math.log10(numRight);
        } else if (operator.equalsIgnoreCase("ln")) {
            //Math.log is to the base e
            //Math.log10 is to base 10
            answer = Math.log(numRight);
        } else if (operator.equalsIgnoreCase("sin")) {
            answer = Math.sin(Math.toRadians(numRight));
        } else if (operator.equalsIgnoreCase("sinh")) {
            //hyperbolic sine
            answer = Math.sinh(Math.toRadians(numRight));
        } else if (operator.equalsIgnoreCase("asin")) {
            //arc sine
            answer = Math.asin(Math.toRadians(numRight));
        } else if (operator.equalsIgnoreCase("cos")) {
            answer = Math.cos(Math.toRadians(numRight));
        } else if (operator.equalsIgnoreCase("cosh")) {
            //hyperbolic cosine
            answer = Math.cosh(Math.toRadians(numRight));
        } else if (operator.equalsIgnoreCase("acos")) {
            //arc cosine
            answer = Math.acos(Math.toRadians(numRight));
        } else if (operator.equalsIgnoreCase("tan")) {
            answer = Math.tan(Math.toRadians(numRight));
        } else if (operator.equalsIgnoreCase("tanh")) {
            //hyperbolic tangent
            answer = Math.tanh(Math.toRadians(numRight));
        } else if (operator.equalsIgnoreCase("atan")) {
            //arc tangent
            answer = Math.atan(Math.toRadians(numRight));
        } else if (operator.equals("*")) {
            answer = numLeft * numRight;
        } else if (operator.equals("(")) {
            //find the closing bracket
            int openBracketCount = 1;
            int index = operatorIndex + 1;

            String bracketedExpression = "";
            for (; index < toEval.length; index++) {

                if (toEval[index].getType() == Token.TokenType.OPERATOR_TOKEN) {
                    if (toEval[index].getStrValue().equals("(")) {
                        openBracketCount++;
                    } else if (toEval[index].getStrValue().equals(")")) {
                        openBracketCount--;
                    }
                } else if (toEval[index].getType() == Token.TokenType.DOUBLE_TOKEN) {
                    bracketedExpression += toEval[index].getDoubleValue();
                }

                if (openBracketCount == 0) {
                    toEval[index] = new Token(Calculator.deleteCommand);
                    break;
                }
                if (toEval[index].getType() == Token.TokenType.OPERATOR_TOKEN) {
                    //if this if statement was placed above openBracketCount--; then the closing bracket would be included in this expression
                    bracketedExpression += toEval[index].getStrValue();
                }
                toEval[index] = new Token(Calculator.deleteCommand);
            }

            //toEval[operatorIndex] = new Token(evaluate(bracketedExpression.substring(0, bracketedExpression.length())));
            answer = evaluate(bracketedExpression.substring(0, bracketedExpression.length()));
        } else {
            System.err.println("Operator not supported: " + toEval[operatorIndex]);
        }

        toEval[operatorIndex + 1] = new Token(Calculator.deleteCommand);
        if (numLeftExists && Operator.isLeftMultipliable(operator)) {
            answer *= numLeft;
            toEval[operatorIndex - 1] = new Token(answer);
            toEval[operatorIndex] = new Token(Calculator.deleteCommand);
        } else if (numLeftExists) {
            //for operators such as +, -, *, /
            toEval[operatorIndex - 1] = new Token(answer);
            toEval[operatorIndex] = new Token(Calculator.deleteCommand);
        } else {
            toEval[operatorIndex] = new Token(answer);
        }
        return toEval;
    //return answer;
    }

    /**
     * tokenizes an mathematical expression string
     * @param expression
     * @return a tokenized array of Tokens
     */
    Token[] tokenizeExpression(String expression) {
        StreamTokenizer st = new StreamTokenizer(new StringReader(expression));
        st.resetSyntax();
        st.parseNumbers();
        //st.wordChars(33,255);

        ArrayList<Token> list = new ArrayList<Token>();

        try {
            int next = 0;
            while ((next = st.nextToken()) != StreamTokenizer.TT_EOF) {
                if (next == StreamTokenizer.TT_NUMBER) {
                    //System.out.println(st.nval);
                    list.add(new Token(st.nval));
                } else {
                    //System.out.println("st.tostring()= "+st.toString());
                    //System.out.println((char)next);
                    list.add(new Token((char) next + ""));
                }
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }

        Token[] tokens = new Token[list.size()];
        Iterator<Token> iterator = list.iterator();
        int tokenIndex = 0;
        while (iterator.hasNext()) {
            tokens[tokenIndex++] = iterator.next();
        }

        tokens = reTokenizeOperators(tokens);

        return tokens;
    }

    /**
     * thorough tokenization of all the operators (this method is called by tokenizeExpression which only able to tokenize the decimals from groups of oprators)
     * this method can tokenize the groups of operators into individual ones.
     * @param tokens
     * @return a tokenized array of Tokens
     */
    Token[] reTokenizeOperators(Token[] tokens) {
        printOutArray(tokens);
        String currTokenStr = "";
        int currIndexHolder = 0;
        int restartTokenizingIndex = 0;
        Token[] tempTokens = null;
        String validOperator = "";
        boolean operatorFound = false;

        for (int index = 0; index < tokens.length; index++) {
            if (tokens[index].getType() == Token.TokenType.DOUBLE_TOKEN) {
                //if the token is a decimal value, then continue
                continue;
            }
            operatorFound = false;
            currTokenStr = tokens[index].toString();
            if (Operator.operatorExists(currTokenStr)) {
                tempTokens = Arrays.copyOf(tokens, tokens.length);
                restartTokenizingIndex = index;
                validOperator = currTokenStr;
                operatorFound = true;
            }
            currIndexHolder = index;
            try {
                /*while (!Operator.operatorExists(currTokenStr)) {
                currTokenStr += tokens[++index];
                System.out.println(currTokenStr);
                tokens[index] = new Token(Calculator.deleteCommand);
                //System.out.println(currTokenStr);
                }*/
                while (currTokenStr.length() < Operator.getLongestOperatorLength()) {
                    //add the next character to the growing currTokenStr
                    currTokenStr += tokens[++index];
                    //System.out.println(currTokenStr);
                    tokens[index] = new Token(Calculator.deleteCommand);

                    //if the currTokenStr exists as an operator, then tempTokens will take on the value of tokens with the delete command appropriately placed
                    if (Operator.operatorExists(currTokenStr)) {
                        tempTokens = Arrays.copyOf(tokens, tokens.length);
                        restartTokenizingIndex = index;
                        validOperator = currTokenStr;
                        operatorFound = true;
                    }
                }
            } catch (ArrayIndexOutOfBoundsException ex) {
                //ex.printStackTrace();
            }
            if (!operatorFound) {
                System.err.println("Invalid operator.");
                System.exit(-1);
            }
            index = restartTokenizingIndex;
            tokens = Arrays.copyOf(tempTokens, tempTokens.length);
            tokens[currIndexHolder] = new Token(validOperator);
        }

        ArrayList<Token> tokensList = new ArrayList<Token>();

        for (int index = 0; index < tokens.length; index++) {
            if (tokens[index].getStrValue().equals(Calculator.deleteCommand)) {
                //remove this token
                continue;
            }
            tokensList.add(tokens[index]);
        }

        //print out tokens BEGIN
        Iterator<Token> tokenIterator = tokensList.iterator();
        while (tokenIterator.hasNext()) {
            System.out.println(tokenIterator.next().toString());
        }
        //print out tokens END

        tokens = new Token[tokensList.size()];
        return tokensList.toArray(tokens);
    }

    /**
     * remove evaluated tokens from tokens array (evaluated tokens are represented by Calculator.deleteCommand)
     * @param tokens
     * @return
     */
    Token[] refreshToEvalArray(Token[] tokens) {
        ArrayList<Token> tokensList = new ArrayList<Token>();

        for (int index = 0; index < tokens.length; index++) {
            if (tokens[index].getStrValue().equals(Calculator.deleteCommand)) {
                //remove this token
                continue;
            }
            tokensList.add(tokens[index]);
        }

        //this.evalTokens = tokensList;

        tokens = new Token[tokensList.size()];
        Iterator<Token> tokenIterator = tokensList.iterator();
        int index = 0;
        while (tokenIterator.hasNext()) {
            tokens[index++] = tokenIterator.next();
        //System.out.println("output: " + tokens[index - 1]);
        }
        return tokens;
    }

    /**
     * method called when there are no more operators => add up all the numbers in tokens array
     * @param tokens
     */
    void noMoreOperators(Token[] tokens) {
        double sum = tokens[0].getDoubleValue();
        for (int index = 1; index < tokens.length; index++) {
            sum += tokens[index].getDoubleValue();
            tokens[index] = new Token(Calculator.deleteCommand);
        }
        tokens[0] = new Token(sum);
        tokens = this.refreshToEvalArray(tokens);
    }

    /**
     * for debugging
     * @param tokens
     */
    void printOutArray(Token[] tokens) {
        for (int i = 0; i < tokens.length; i++) {
            System.out.println("tokens[" + i + "] = " + tokens[i]);
        }
    }
}
